Skip to content

docs(skills): teach controlled_by_parent with an optional master in the data skill, permission docs and ADR-0055 - #31092

Merged
os-zhuang merged 3 commits into
mainfrom
claude/project-thread-6r15z0
Nov 7, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/project-thread-6r15z0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Irene · project thread

Fixes #30907
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:数据技能(skills/objectstack-data 的 rules/relationships.md、rules/security.md、references/lint-rules.md、SKILL.md)、五个权限文档页和 ADR-0055 现在教「可选主表」:controlled_by_parent 子对象把它唯一的 master_detail 写成 required: false,不再教「复制一个属主字段再用钩子同步」。没挂主表的行归属主:属主或子对象「查看全部」可读,属主或子对象「修改全部」可写;新建不挂主表的行直接放行;挂主表要对新主记录有编辑权,清空主表要对原主记录有编辑权(ownerMayEdit 不覆盖);链上某跳没有主表就在那里按同一规则结束。另加一句:成员键的 valueField 不能是名册对象自己的 id(membership-key-value-field-invalid)。ADR-0055 新增 2026-11-07 的 §3 修订,并就地标注 2026-11-06 那行「§3 不变」。
  • 为什么改:您对 controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 的裁决「A」分三张卡落地;第 1、2 张(PR feat(spec,security): a controlled_by_parent object may declare an optional master #30940、feat(security, verify): a controlled_by_parent row with no master is written by its owner or by Modify All on the detail #31082)已合并,平台已支持可选主表的读和写,但技能和文档还在教「主表必须必填」或复制属主字段的老路。这是第 3 张。
  • 风险与回滚:只改技能文本、文档、ADR 和一个 changeset,无运行时影响;四个技能文件都在 token 天花板内(棘轮行不动);回滚即 revert。
  • 先看:skills/objectstack-data/rules/relationships.md 新增的「Optional master」小节,以及 ADR-0055 末尾的 2026-11-07 修订。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期 exit 0,读数见下文 Token budget。
  • 需要你做的:skills/** 和 docs/adr/** 属 Tier H,需要 os-zhuang 或 hotlong 批准。

What changes, and the code behind each taught fact

Every fact was read on main 719a502b (after PR #30940 for #30905 and PR #31082 for #30906):

Taught Source on main
The object's only master_detail declared required: false is the master when no required one stands (an optional master) packages/spec/src/data/controlled-by-parent-master.ts controlledByParentMasterOf, judged by controlledByParentMasterRefusal (the parse's refinement in object.zod.ts)
Still refused: required: false beside a second master_detail; a readonly / system master (required or optional); an omitted required is forced to true by ObjectSchema.create() same module (several-masters, master-not-writable); object.zod.ts forceCbpMasterDetailRequired; lint relationship/master-detail-required (packages/lint/src/data-model-rules.ts cbpMasterReferenceFinding)
A null-master row is read by its owner (owner_id) and by View All on the detail; no owner / no owner_id column ⇒ View All alone packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts computeControlledByParentFilter (the nullMasterArm)
A null-master row is written by its owner or Modify All on the detail (everyone else 403, View All included); no owner ⇒ Modify All alone security-plugin-cbp-write-gate.ts nullMasterRowWritable, and the masterId == null && rel.optional block of assertControlledByParentWrite
An insert naming no master is admitted same block: if (operation === 'insert') return;
Setting a master asks edit on it; emptying one asks edit on the stored master, and ownerMayEdit does not cover it askBodyNamedMaster after the owner rule; the ownerMayEdit leg's !(rel.optional && bodyEmptiesMasterReference(...))
A chain hop with no master ends there under the same rule the chain walk's nextMasterId == null && nextRel.optional branch (nullMasterRowWritable on the master's own object)
A membership key's valueField cannot be the roster's own id packages/lint/src/validate-membership-key-declarations.ts: columnOf maps id to an empty slice, valueFieldFault reports it membership-key-value-field-invalid

Skills. rules/relationships.md gains § Optional master (after the ownerMayEdit section), and its master_detail table row names it. rules/security.md: the "choose by what the rows are keyed on" list gains "a parent's owner, parent optional → an optional master", and the valueField note gains the id refusal. SKILL.md: the OWD table's controlled_by_parent row says a row with no (optional) master is its owner's. references/lint-rules.md: the relationship/master-detail-required row names the optional master.

Docs (content/docs/permissions/): authorization.mdx (the lint-gate bullet, ≈L539: one master, required or optional, and what no-relation / ambiguous-relation now catch), rls.mdx (the controlled-by-parent bullet), sharing-rules.mdx (OWD table row), permissions-matrix.mdx (OWD table row), index.mdx (a comment on the example's contact: 'controlled_by_parent').

ADR-0055. A new section "Amendment (2026-11-07, #30906): the null-master row its owner writes", a status line for it, and an inline ⚠️ note at §3. The 2026-11-06 status line's "§3 … is unchanged" is marked in place (⚠️ Amended 2026-11-07), as are the three sentences inside the 2026-11-06 amendment that said the same (its Ruling paragraph's "amended again for §3 then", its Fail-closed bullet on the write gate, its "What is unchanged" §3). Nothing is rewritten.

Token budget

node scripts/check-skills-token-ratchet.mjs (convention ceil(utf8 bytes / 4)), no ratchet row moved, scripts/check-skills-token-ratchet.mjs untouched:

File main head ceiling
skills/objectstack-data/SKILL.md 6078 6071 6078
skills/objectstack-data/rules/security.md 3688 3696 3698
skills/objectstack-data/rules/relationships.md 3838 3870 3873
skills/objectstack-data/references/lint-rules.md 935 958 970

(The order's SKILL.md reading of 6068 had moved to 6078/6078 on main 719a502b.) No copied-owner-field recipe existed in these files to replace, so the growth is paid by deleting restatement in the same file; lint-rules.md's +23 is one table row and stays under its ceiling with no deletion. Each deletion and its surviving home:

rules/relationships.md

  1. The intro "Guide for modeling relationships between objects using lookup, master_detail, and junction patterns." Home: the H1 and the ToC lines above it.
  2. § Delete Behaviors' fence { type: 'master_detail', reference: 'parent_object', deleteBehavior: 'cascade', // or 'restrict' — 'set_null' is refused here }. Home: the sentence and table right above it ("cascade or restrict only; an authored set_null is refused").
  3. § Roll-up Summaries "and never summed on the client". Home: "server-side" in the same sentence.
  4. § Roll-up Summaries "Pairs naturally with inline editing (below): the parent total updates atomically as line items are saved." Home: the same section's "inside the same transaction as the write" and § Inline Editing's "saves parent + children in one atomic transaction".
  5. § Inline Editing "— with no form-view config and no bespoke page". Home: the section's first sentence ("in the data model — not in a form view").
  6. § Inline Editing "but the relationship inlineEdit is the primary, zero-config path". Home: the same first sentence.
  7. ❌ Incorrect's comment // Invoice line items should NOT be independent. Home: the heading and the fence's own // ❌ Child can exist without parent — wrong!.
  8. ✅ Correct's fence (invoice_line_item with a required master_detail, deleteBehavior: 'cascade'), replaced by a link to § One-to-Many: master_detail, which declares the same object and field.

rules/security.md

  1. The RLS example's comments // rows readable / targetable and // every row written. Home: the prose right above ("using admits rows …", "check is judged on every row an insert/update writes").
  2. § Membership keys intro "rather than on a person or a position". Home: the same section's "Choose by what the rows are keyed on" list.

SKILL.md

  1. "See rules/relationships.md for detailed examples." under the Relationship Patterns table. Home: the Quick Reference link to Relationships.

Eval sweep

grep over skills/objectstack-data/evals/*.json for controlled_by_parent, master_detail, required, owner, hook, copy/mirror/stamp and membership: no case pins the old rule. objects-fields-relationships.json case 2 expects required: true on an invoice line's master_detail, which stays right (a line always has its invoice). hooks-security-seeds-datasources.json case 6 (membership keys for portal orders) and case 1 (the search-mirror hooks) teach rules this card leaves standing. No eval file is changed.

Scorer check (read-only)

packages/qa/scenarios/src/score/scenario-tags.ts row-scope check, now at L393 (the card's L343 drifted): if (effectiveSharingModel(def) === 'private' || def.sharingModel === 'controlled_by_parent') return check(at, true, model);. It accepts any controlled_by_parent object without reading its master's required, so an optional-master detail passes. No change.

Spec contract jsdoc owed (spec lane, not edited here)

The wording packages/spec/src/contracts/security-service.ts owes (master_chain, master_reference_missing, checkControlledByParentWrite) is filed for the spec lane as #31094.

Other content/docs pages that still teach a required-only master

Listed, not widened: filed as #31095 (error-handling.mdx, error-catalog.mdx, cli.mdx, including the two sites the contract review added). Release pages (content/docs/releases/**) are release-owned.

Review round 1

Contract review FAIL at 197aa7c88 (comment 6703355433). Fixed in delta commit 9cea5ade1, as the seat ordered (one new commit, no rebase or force-push):

The delta costs rules/relationships.md 6 tokens (3865 → 3871, ceiling 3873); no deletion was added and no ratchet row moved.

Review round 2

Delta review PASS at 9cea5ade1 (comment 6703520499), no must-fix. Its should-fix S5, closed here as the seat ordered, in delta commit a60b01a6e: rules/relationships.md § Owner edits opened "A controlled_by_parent detail inherits the master's access: reads are scoped to accessible masters", which left out the null-master read (the owner and View All, § Optional master). It now reads "A controlled_by_parent detail row naming a master inherits its access: reads are scoped to accessible masters and every write needs edit on the master", so the read and the write halves both speak of a row that names a master (S1's write qualifier moves into the subject). The change is net −1 token (3871 → 3870, ceiling 3873); no deletion was needed and no ratchet row moved.

Review tier

dispatch-gates mandates the contract-review tier for skills/** and docs/adr/** (clause ①); that is waived by the maintainer's standing order in this project (2026-11-03). The seat runs the contract review.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on head a60b01a6e (11 paths vs merge base 719a502b2): 75 commands, the same list as rounds 1 and 2, each run sequentially under timeout 1200 with NODE_OPTIONS=--max-old-space-size=8192 (raised from the cloud container's default heap) over the full pnpm build (119/119 tasks), exit code captured before any pipe → 75 × exit 0, none re-run. --ran reconciliation: 75 derived famil(ies) accounted for — 75 run, 0 NOT-MEASURED. Rounds 1 (197aa7c88) and 2 (9cea5ade1) were also 75 × exit 0. origin/main has moved since, touching none of these paths, so no merge was needed. node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD → exit 0 before each push.

Ran list (head a60b01a)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 0
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-docs :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:adr-anchors :: exit 0
pnpm check:adr-index :: exit 0
pnpm check:adr-symbol-anchors :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-compatibility :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset ('@objectstack/skills': patch), and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is the maintainer's Chinese summary the fleet asks for.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8VVAo8GHSrxa8ZR8h3qxs

…he data skill, permission docs and ADR-0055

The data skill, five permission docs pages and ADR-0055 now teach the
optional master (a controlled_by_parent detail whose only master_detail is
declared required: false) instead of an owner field copied by hooks: a row
with no master is read by its owner or View All on the detail and written by
its owner or Modify All on it; an insert naming no master is admitted;
emptying a master asks edit on the stored master; a chain hop with no master
ends there under the same rule. ADR-0055 gains a dated 2026-11-07 amendment
for the section 3 write rule, and the 2026-11-06 header line that said
section 3 was unchanged is marked in place. The membership-key notes say a
valueField cannot be the roster's own id.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAo8GHSrxa8ZR8h3qxs
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 7, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 197aa7c · 2026-11-07T06:17Z

The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent. No review seat (lanes/review.md) is seated.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 197aa7c88ecb765efa7827b07e42a1f3047a4eb2
Local-runs: none
Scope: docs/adr/0055-master-detail-controlled-by-parent.md, skills/objectstack-data/SKILL.md, skills/objectstack-data/references/lint-rules.md, skills/objectstack-data/rules/relationships.md, skills/objectstack-data/rules/security.md + content/docs/permissions/authorization.mdx, content/docs/permissions/rls.mdx, content/docs/permissions/sharing-rules.mdx, content/docs/permissions/permissions-matrix.mdx, content/docs/permissions/index.mdx, .changeset/data-skill-optional-master.md, skills/objectstack-data/evals/*.json; cited code, read on origin/main 51992dff6 (none of these paths moved since merge base 719a502b2): packages/spec/src/data/controlled-by-parent-master.ts, packages/spec/src/data/object.zod.ts (forceCbpMasterDetailRequired, refuseControlledByParentWithoutOneMaster), packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts, packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts, packages/lint/src/data-model-rules.ts (cbpMasterReferenceFinding), packages/lint/src/validate-security-posture.ts (cbpMasterCandidates), packages/lint/src/validate-membership-key-declarations.ts, packages/qa/scenarios/src/score/scenario-tags.ts, packages/spec/src/contracts/security-service.ts, packages/verify/src/parent-write-reach.ts, scripts/check-skills-token-ratchet.mjs, scripts/pm/check-governed-merges.mjs, scripts/pm/check-governed-queue-guard.mjs; and the other content/docs pages that still teach a required-only master (error-handling.mdx, error-catalog.mdx, cli.mdx)

① Derived judgments

Inputs: card #30907 (body; the triage; the card-2 carry-over 6702057019; the unlock; the claim), sibling #30906 with PR #31082 (its body and its changeset 30906-cbp-null-master-write-gate.md), the PR body, its 11-file list and the net diff against merge base 719a502b2. Each taught fact was checked against the code on main:

  • Which master_detail may be required: false. TRUE. controlledByParentMasterOf: exactly one required: true wins; otherwise the object's only master_detail, if it is === false, is the optional master. Refused: readonly/system on the one master, required or optional (master-not-writable). An omitted required is forced to true by ObjectSchema.create() (forceCbpMasterDetailRequired). Two master_detail with none required is refused (several-masters). The skill's "Refused beside a second master_detail" is true at create(), which throws for required: false beside any other master_detail. That is the door the skill teaches. (A raw parse admits it beside a required master as an ordinary parent, and that is not the master. Not a finding.) The lint-rules row matches cbpMasterReferenceFinding (soleMasterDetail && required === false passes; flags still reported).
  • Who reads a null-master row. TRUE. computeControlledByParentFilter: nullMasterArm is {fk: null} under hasSuperuserReadBypass (View All or Modify All on the detail). Otherwise it is {fk: null} AND owner_id = caller when ownerColumn, else no arm. The master arm is OR-ed beside it. Skill, rls.mdx, sharing-rules.mdx, permissions-matrix.mdx and SKILL.md all say "the owner and View All".
  • Who writes one. TRUE. nullMasterRowWritable admits hasSuperuserWriteBypass (Modify All), else owner_id non-null and equal to the caller. With no owner column, Modify All only. View All alone is refused (nullMasterOwnerDenied, leg master_chain).
  • An insert naming no master is admitted. TRUE. if (masterId == null && rel.optional) { if (operation === 'insert') return; …. Batch rows that do name a master are still asked.
  • Emptying a master asks edit on the stored master, and ownerMayEdit does not cover it. TRUE in code. masterId is the stored master, so the §3 walk asks edit on it. The owner leg carries !(rel.optional && bodyEmptiesMasterReference(opCtx.data, rel.fk)), and askBodyNamedMaster lets an optional master's null through. The ADR amendment, the PR body and the changeset all state it plainly. ⛔ must-fix M1, rules/relationships.md § Optional master: "emptying one asks edit on the stored master, ownerMayEdit aside." Read in the usual sense of "X aside" ("apart from X"), this makes ownerMayEdit an exception, so an opted-in owner would be able to empty with only read on the master. That is false, and it inverts the one fact card 2 pinned with its own ablation (A2). This file is what authors and agents read, so the sentence must say it directly. Suggested: "…asks edit on the stored master, even under ownerMayEdit." That is about +1 token, and the file has 8 of headroom.
  • Chain hop. TRUE. assertControlledByParentChainEditable: nextMasterId == null && nextRel.optional, then nullMasterRowWritable on the master's own object, else master_chain. "Ends there under the same rule" is accurate.
  • Membership-key line. TRUE. columnOf maps id to an empty meta slice, so valueFieldFault reports a non-reference and the rule is membership-key-value-field-invalid.
  • authorization.mdx lint bullet. TRUE against cbpMasterCandidates: required masters first; with none, the spec's optional master. no-relation covers none, two none-required, an omitted required, and a required lookup. ambiguous-relation covers two or more required.
  • ADR-0055 §3 amendment against what card 2 shipped. It matches the feat(security, verify): a controlled_by_parent row with no master is written by its owner or by Modify All on the detail #31082 body and changeset point for point: the owner rule, insert, by-id and predicate writes, setting (owner rule then edit on the new master), emptying (the §3 walk, opt-in excluded), chains, on-behalf-of (each pass its own principal), checkControlledByParentWrite answering allow/deny on master_chain (never unresolvable), proof via parent-write-reach.ts masters: 'none' (present on main), and a required master unchanged (422). It does not overclaim the chain-hop proof (plugin-security suite only, per feat(security, verify): a controlled_by_parent row with no master is written by its owner or by Modify All on the detail #31082).
  • In-place marks. They are honest. Each of the four 2026-11-06 sentences (the status line; Ruling "amended again for §3 then"; Fail-closed "the write gate is unchanged"; What is unchanged "§3, the chain walk") keeps its original text byte for byte, with a dated ⚠️ note appended. The §3 rule line gains a second ⚠️ note beside the 2026-10-26 one. No history is rewritten.
  • The five docs pages. They agree with the code and with each other (owner + View All reads; owner + Modify All writes). index.mdx's comment cites "ADR-0055 §1" for ownership. It is loose (the owner rule is §2/§3) but not false.
  • Deleted restatements. All 11 were checked against their named homes, and each home carries the fact. relationships.md: the intro line repeats the H1 and ToC. The Delete Behaviors fence is covered by "cascade or restrict only; an authored set_null is refused". "Never summed on the client" is covered by "server-side". "Pairs naturally…" is covered by "inside the same transaction" plus Inline Editing's "one atomic transaction". "No form-view config…" and "primary, zero-config path" are covered by "on the relationship, in the data model — not in a form view". The ❌ comment is covered by the fence's own comment. The ✅ fence is the same object as the § One-to-Many: master_detail fence (invoice_line_item, required, cascade). security.md: the RLS comments are covered by the using/check prose directly above. "Rather than on a person or a position" is covered by the "Choose by what the rows are keyed on" list. SKILL.md: the dropped link is the Quick Reference L42 link. No unique fact is lost.
  • Scorer (read-only). scenario-tags.ts L393 accepts any def.sharingModel === 'controlled_by_parent' without reading required, so no change is needed. That is correct.
  • should-fix S1, rules/relationships.md § Owner edits: "every write needs edit access on the master" and "insert … still need master edit" now contradict the next subsection for a row that names no optional master (an insert is admitted; the owner rule governs). Qualify them, e.g. "of a row that names its master". Token headroom is tight; a same-file restatement deletion can pay for it.
  • should-fix S2, ADR-0055 2026-11-07 amendment, owner-rule bullet: "Everyone else, View All included, is refused 403" is the gate's verdict. A principal who cannot read the row meets the read door's 404 RECORD_NOT_FOUND at step 2.7 first (pinned in feat(security, verify): a controlled_by_parent row with no master is written by its owner or by Modify All on the detail #31082). Narrow it to "everyone else who reaches the gate" or add the 404.

② Semver level

'@objectstack/skills': patch is right. Only skill prose ships (packages/skills is the published package, in the fixed group); content/docs and docs/adr do not publish, and the platform behaviour shipped in #30940/#31082. This matches the triage's prescription and the precedent for skill-text changesets. Clause-②: no and Metadata-protocol: none are right (no schema, contract or metadata meaning moves), and they are line-initial in both the PR body and the changeset.

③ Boundary flags

  • Diff against the claim. The diff stays inside the claim: the four data-skill files, the five permissions pages, ADR-0055 and one changeset. No eval file changed, the ratchet file is unchanged against main, and packages/spec is untouched (security-service.ts included).
  • Additional sentences in the 2026-11-06 amendment. They are within "the ADR". The claim's parenthetical names only the header line, but they are the same file, and carry-over item 1 names those body sentences ("still says §3 is unchanged and 'is amended again for §3 then'"). All edits are append-only marks.
  • Tier. check-governed-merges GOVERNED_SURFACES: skills/** (row skills-catalog) and docs/adr/** (row adr) are GOVERNED_TIER_H. One Tier H path makes the whole PR Tier H. Landing needs a human merge or an approval from GOVERNED_APPROVERS (os-zhuang, hotlong). The PR body says so, and mergeable_state is blocked.
  • Spec jsdoc owed in the PR body. The wording is accurate against security-service.ts on main. L342–348 master_chain omits the record-itself null-master refusal and the optional hop. L377–378 master_reference_missing should say "required". L1064–1065 checkControlledByParentWrite should name the owner/Modify All alternative. Each matches the gate (nullMasterOwnerDenied → master_chain; MasterReferenceMissingError is only reached for a required master).
  • should-fix S3, the other-docs list in the PR body is incomplete:
    • error-handling.mdx L286, 559–562 and 590–596, error-catalog.mdx 189–196, 214–216 and 1014, and cli.mdx L2142 are correct as listed.
    • Missing: content/docs/deployment/cli.mdx L2707–2711. The --parent-write-reach paragraph says a write is "admitted exactly when the caller holds that verb … and can edit its master", but verify now also proves null-master rows by the owner rule.
    • Missing: content/docs/protocol/kernel/error-handling.mdx L568–577. The declaration table lists required shapes only, and "Only the first row is a declaration the parse accepts" no longer holds: required: false on the sole master_detail is accepted, and emptying it is admitted under the owner rule.
    • Add both to the list and to the follow-up.
  • should-fix S4, carry-over item 2. The claim says the seat files the spec-lane card for the security-service.ts jsdoc, but no such card is linked on controlled_by_parent admits an optional master (3/3): docs and the authoring skills teach it instead of a copied owner field #30907's thread. File it and link it from the card or the PR before merge, so the owed wording does not get lost.

④ Design judgment

  • One home per rule. Yes. The optional master has one home, rules/relationships.md § Optional master. security.md's chooser line, SKILL.md's OWD row and lint-rules.md's row point to it or summarise it in one clause. The ADR keeps the mechanism and the docs pages carry one-line summaries.
  • Token budget. ceil(bytes/4), SKILL.md version line excluded, ceilings read from check-skills-token-ratchet.mjs on main, no row raised:
File main head ceiling
SKILL.md 6078 6071 6078
security.md 3688 3696 3698
relationships.md 3838 3865 3873
lint-rules.md 935 958 970

All four are within ceiling; the readings match the PR body.

  • Eval sweep. No case pins a required master for the optional-master scenario, and none pins a copied owner field. objects-fields-relationships.json ✨ Set up Copilot instructions #2 keeps required: true on an invoice line (right: a line always has its invoice), and its "every other write still needs master edit" is true for a required master. hooks-security-seeds-datasources.json Add metamodel interfaces for ObjectQL/ObjectUI contract #1 is a search mirror, not an owner copy, and Migrate from npm to pnpm #6 (membership keys, "no owner stamp per order") is still right.
  • Against the card's scenario. "A rep sees the contacts of the accounts they own; a contact need not have an account" is taught as account: { type: 'master_detail', reference: 'account', required: false } on a controlled_by_parent contact. That is what main admits and serves. It is sound apart from M1's wording.
  • Check-runs on this head, read 2026-11-07T06:23Z: 43 runs, 29 success, 12 skipped, 0 failed. Test Core (1/8) and Test Core (2/8) are still in progress.

Must-fix: M1. Should-fix: S1, S2, S3, S4.

Implemented-by: claude/project-thread-6r15z0
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: FAIL

…ster, and scope the master-edit rule to rows that name one

The optional-master section said emptying a master asks edit on the stored
master "ownerMayEdit aside", which reads as an exception; it now says "even
under ownerMayEdit". The owner-edit section's "every write needs edit access
on the master" and "still need master edit" now speak of rows that name a
master. ADR-0055's 2026-11-07 amendment says the 403 is for a principal who
reaches the gate; one who cannot read the row meets the read door's 404 first.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAo8GHSrxa8ZR8h3qxs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 9cea5ad · 2026-11-07T06:41Z

Delta review against the FAIL at head 197aa7c (comment 6703355433: must-fix M1, should-fixes S1–S4). The delta is one commit to relationships.md and ADR-0055 answering M1, S1 and S2, plus the PR body (patched by the seat). S3 and S4 are filed as #31095 and #31094. The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9cea5ade1ac2b88a4bfe249ce0252266d842d300
Local-runs: none
Scope: docs/adr/0055-master-detail-controlled-by-parent.md, skills/objectstack-data/SKILL.md, skills/objectstack-data/references/lint-rules.md, skills/objectstack-data/rules/relationships.md, skills/objectstack-data/rules/security.md + the delta 197aa7c88..9cea5ade1 (relationships.md, ADR-0055); the PR body as patched; issues #31094 and #31095; code read on origin/main b63157335 (it moved from 51992dff6, touching none of skills, docs/adr, content/docs, plugin-security, spec, lint, verify, the ratchet or check-governed-merges): packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts (assertControlledByParentWrite, nullMasterRowWritable, bodyEmptiesMasterReference, the owner leg, askBodyNamedMaster, the chain walk), packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts (step 2.7, callerCannotReadAddressedRow, recordNotFoundError), packages/spec/src/contracts/security-service.ts, scripts/check-skills-token-ratchet.mjs

① Derived judgments

This is a delta review against the FAIL at 197aa7c88 (M1, S1–S4). There is one new commit, 9cea5ade1, with 4 lines changed in 2 files (relationships.md, ADR-0055). The net diff against main is still the same 11 files, and no other file moved.

  • M1: answered, TRUE. § Optional master now reads "emptying one asks edit on the stored master, even under ownerMayEdit". On main, the owner leg carries !(rel.optional && bodyEmptiesMasterReference(opCtx.data, rel.fk)), so an emptying body skips the read-on-master leg. It then takes assertControlledByParentChainEditable on the stored masterId, which is edit on the master. The ambiguous reading is gone.
  • S1: answered, TRUE. The § Owner edits sentences are now scoped to rows that name a master.
    • "every write of a row naming a master needs edit on it": the gate asks the walk of the stored master on a by-id write, of each master a predicate write selects, and of the master an insert names. A re-pointed body is also asked of its new master (askBodyNamedMaster).
    • "insert, delete, predicate (multi: true) updates and on-behalf-of writes under a master still need its edit": the owner leg is by-id update only, and !opCtx.context.onBehalfOf.userId. Every other shape under a master takes the walk.
    • Neither sentence now contradicts § Optional master, where an insert naming no master is admitted and the owner rule governs a null-master row.
  • S2: answered, TRUE for the shape it names. The owner-rule bullet now says 403 "to everyone else who reaches the gate". It adds that a principal who cannot read the row meets the read door first (404 RECORD_NOT_FOUND, step 2.7). On main, step 2.7 (security-plugin-authz-middleware.ts, security(data): a by-id write answers 403 for a row the caller cannot read and 404 for an id that does not exist, for principals the write pre-image check does not bind: an existence signal the read door withholds #21771) asks callerCannotReadAddressedRow for the by-id write the caller addressed, and throws recordNotFoundError before the 2.8 gate runs. This holds for every principal class, which is the "as for every hidden row" the sentence claims. A principal whose read of the whole object is refused gets that refusal instead, and a predicate write never selects a row its caller cannot read. The sentence speaks of "the row", an addressed row, so neither case makes it false. Not a finding.
  • New false or over-broad claims added by the delta: none. The three changed sentences were each read against the code above, and the rest of the delta is the same text.
  • should-fix S5 (it predates this round; this delta did not add it; it does not block): § Owner edits still opens with "reads are scoped to accessible masters". That is incomplete for an optional master's null-master row, which its owner and View All read (computeControlledByParentFilter's nullMasterArm). This is the read twin of S1. The next subsection states the read rule, so the gap is one hop away. relationships.md has 2 tokens of headroom, so a fix needs a same-file deletion. Fix it here or carry it with docs: error-handling, error-catalog and cli pages still say a controlled_by_parent master is always required (optional master, cards #30905/#30906) #31095.

② Semver level

Unchanged from round 1. The changeset is still '@objectstack/skills': patch, with Clause-②: no and Metadata-protocol: none line-initial in the body and the changeset. The delta is skill and ADR prose only. That is right.

③ Boundary flags

  • The delta stays inside the claim. It touches relationships.md and ADR-0055, both claimed files. packages/spec is untouched, no eval file changed, and the ratchet script is identical to main.
  • S3: carried by docs: error-handling, error-catalog and cli pages still say a controlled_by_parent master is always required (optional master, cards #30905/#30906) #31095. Adequate. The issue is open and labelled finding. It lists every site from round 1, including the two this review added: error-handling.mdx L568–577 (the declaration table and "Only the first row…") and cli.mdx L2707–2711. It also has the original error-handling L286/559–562/590–596, error-catalog L189–196/214–216/1014, and cli L2142. It says what changed on the platform, excludes release pages, and names this PR's pages as the wording to agree with. The cli L2707–2711 entry names the paragraph but not its stale clause ("admitted exactly when … can edit its master"). The line range is enough to find it.
  • S4: carried by spec: security-service contract jsdoc still describes a controlled_by_parent master as always required (optional master, cards #30905/#30906) #31094. Adequate. The issue is open and labelled finding. Its three owed rewordings match the ones verified in round 1 against security-service.ts and the write gate (master_chain L342–348, master_reference_missing L377–378, checkControlledByParentWrite L1064–1065). It routes the work to the spec lane, jsdoc only. It proposes an '@objectstack/spec': patch with Clause-②: no, which triage confirms.
  • The body points to both issues and drops the two inline lists.
  • Tier: unchanged, H. skills/** and docs/adr/** need a human merge or an approval from os-zhuang or hotlong, and the PR's mergeable_state is blocked.

④ Design judgment

  • Token budget (ceil(bytes/4), ceilings read from main, no row raised):
File Head Ceiling
rules/relationships.md 3871 3873
SKILL.md 6071 6078
rules/security.md 3696 3698
references/lint-rules.md 958 970

relationships.md matches the claimed 3871/3873, a delta of +6 with no deletion. The SKILL.md figure has its version line excluded.

Must-fix: none. Should-fix: S5 (it predates this round and does not block). Round 1's M1, S1 and S2 are answered in the diff, and S3 and S4 are carried by #31095 and #31094.

Implemented-by: claude/project-thread-6r15z0
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

…ame a master

The ownerMayEdit section opened with "a controlled_by_parent detail inherits
the master's access: reads are scoped to accessible masters", which left out
the null-master read the optional-master section teaches. It now speaks of a
detail row naming a master, for reads and writes alike.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAo8GHSrxa8ZR8h3qxs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head a60b01a · 2026-11-07T06:57Z

Delta review against the PASS at head 9cea5ad (comment 6703520499). The delta is one commit to relationships.md answering that review's S5 (the owner-edit section's read rule scoped to rows that name a master), plus the PR body (patched by the seat). The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a60b01a6eaddce9cd91fc9b7db1773dc45d1b4d4
Local-runs: none
Scope: docs/adr/0055-master-detail-controlled-by-parent.md, skills/objectstack-data/SKILL.md, skills/objectstack-data/references/lint-rules.md, skills/objectstack-data/rules/relationships.md, skills/objectstack-data/rules/security.md + the delta 9cea5ade1..a60b01a6e (relationships.md § Owner edits); the PR body as patched; code read on origin/main b63157335: packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts (computeControlledByParentFilter, its master arm and nullMasterArm), packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts (assertControlledByParentWrite, the owner leg, the masterId == null && rel.optional block), scripts/check-skills-token-ratchet.mjs

① Derived judgments

This is delta review 2, against the PASS at 9cea5ade1, which carried one should-fix, S5. The delta is one commit, a60b01a6e, with 2 lines changed in relationships.md § Owner edits. The net diff against main is still the same 11 files, and nothing else moved. 197aa7c88 and 9cea5ade1 are ancestors of this head, so nothing was force-pushed.

  • S5: closed, TRUE. The opening sentence now reads "A controlled_by_parent detail row naming a master inherits its access: reads are scoped to accessible masters and every write needs edit on the master". The subject is now a row that names a master, so both halves speak of that row.
    • The read half: such a row is matched only by the master arm, { fk: { $in: ReadableIds.of(master) } }. The null-master arm matches only { fk: null }, and the two arms never share a row.
    • The write half: a by-id write of such a row takes the walk on its stored master, a predicate write takes the walk on each selected master, and an insert naming one is asked of it. A re-pointing body is also asked of its new master. The only exception is the ownerMayEdit leg, which the very next sentence introduces as the opt-in. So "every write needs edit on the master" holds as the default the section then narrows. It matches S1's former qualifier, which moved into the subject.
  • Consistent with the rest of the file and with security.md.
    • § Optional master still states the null-master rules: the owner or View All reads, the owner or Modify All writes, an insert naming none is admitted, emptying asks edit on the stored master even under ownerMayEdit, and a chain hop ends under the same rule.
    • "Nothing else widens — … writes under a master still need its edit" is unchanged and agrees with the new subject.
    • security.md's chooser line ("a parent's owner, parent optional → an optional master") is unchanged and does not conflict.
  • New false or over-broad claims: none. The delta is this one sentence, re-read against both functions above.

② Semver level

Unchanged. The changeset is still '@objectstack/skills': patch, with Clause-②: no and Metadata-protocol: none line-initial in the body and the changeset. The delta is skill prose only. That is right.

③ Boundary flags

④ Design judgment

  • Token budget (ceil(utf8 bytes / 4), the convention the ratchet's own header states):
File Head Ceiling
rules/relationships.md 3870 3873
SKILL.md (version line excluded) 6071 6078
rules/security.md 3696 3698
references/lint-rules.md 958 970

relationships.md is 3870 of 3873, which holds the claimed reading (−1 from 3871). The figures were measured from git show bytes under the read-only shape, not by running the script on a worktree; the arithmetic is the script's.

  • Line cap 120 B: holds. The two changed lines are 89 B and 70 B. No added line anywhere in the net diff exceeds 120 B.
  • The body matches the diff. The token row is updated (3871 → 3870). The "Review round 2" section quotes the committed sentence exactly and states −1 token with no ratchet move. The gate section names head a60b01a6e.
  • One home per rule: unchanged.
  • Check-runs on this head (read 2026-11-07T06:58Z): 45 runs, 18 success, 14 skipped, 0 failed.
    • Still in progress (13): Build Core, Build Docs, Check Changeset, the four Lint jobs, Live E2E UI, Test Core (1/8), (2/8) and (3/8), and Type Check · consumer gates and · source gates.
    • The previous head 9cea5ade1 finished green: 34 success, 14 skipped, 0 failed.

Must-fix: none. Should-fix: none. S5 is closed. Round 1's M1, S1 and S2 stay answered, and S3 and S4 stay carried by #31095 and #31094.

Implemented-by: claude/project-thread-6r15z0
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

维护者速读

席位核对 — ACCEPT · domain:skills#3 · head a60b01a6eaddce9cd91fc9b7db1773dc45d1b4d4

  1. 形态: draft → main,Fixes #30907;'@objectstack/skills': patch,Clause-②: no,Metadata-protocol: none;作者 objectstack-fleet[bot];11 个文件,在认领面之内(四个技能文件、五个权限文档页、ADR-0055、changeset)。
  2. 前提实测: 复核逐项对照 main 代码:object.zod.ts 的 master-not-required 细化、security-plugin.ts 的 computeControlledByParentFilter 空主表分支与写门、相关 lint 规则;scenario-tags.ts 只读核对结果见 PR 正文。
  3. 门禁(钉 head a60b01a6): dev 派生门禁 75 条全部 exit 0,0 条未测;check-runs 48 条:34 success、14 skip、0 失败;棘轮读数 relationships.md 3870/3873、SKILL.md 6071/6078、security.md 3696/3698、lint-rules.md 958/970,无行变动;新增行均 ≤ 120 B。check-governed-merges --pr 31092:Tier H(skills/**、docs/adr/**)。
  4. 接受的偏差: 无。首轮 FAIL 的 M1、S1、S2 与增量一的 S5 已在新提交改正并经增量复核确认;PR 正文由本席按新 diff 更新。
  5. 范围外去向: S3 → docs: error-handling, error-catalog and cli pages still say a controlled_by_parent master is always required (optional master, cards #30905/#30906) #31095(其他 content/docs 页),S4 → spec: security-service contract jsdoc still describes a controlled_by_parent master as always required (optional master, cards #30905/#30906) #31094(security-service.ts jsdoc,spec 车道)。
  6. 落地条款: Tier H。保持 draft,挂 pm:awaiting-merge,请 os-zhuang、hotlong 审批;获批后本席过落地前检、清标、ready、auto-merge;维护者直合亦可。

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 7, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 7, 2026 10:38
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit aa3e21c Nov 7, 2026
54 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-6r15z0 branch November 7, 2026 10:49
@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

controlled_by_parent admits an optional master (3/3): docs and the authoring skills teach it instead of a copied owner field

2 participants